On this page

Skip to content

How to Use AutoMapper in .NET

Introduction

When designing an application, it is common to use a multi-layered architecture. Adhering to the principle of separation of concerns, one generally does not transmit the same DTO across multiple layers. Instead, the incoming DTO is converted into another DTO between layers before being passed to the next. For example, you might have a split like this: Entity ← (Domain Layer) → Service DTO ← (Application Layer) → ViewModel. When performing data updates, the Application Layer converts the ViewModel into a Service DTO and passes it as a parameter to the Domain Layer's Service. The Service then converts the Service DTO into an Entity and writes it to the database via a Repository (Entity Framework itself implements the Repository Pattern). Handling the property/field assignments for these DTO conversions is very cumbersome, so developers often write mapping (Reflection) APIs to simplify the operation. AutoMapper is a commonly used mapping package, and its documentation is very detailed and relatively easy to get started with.

How to Use AutoMapper

The usage of AutoMapper is as follows:

csharp
// Create a MapperConfiguration to define the mapping relationship between classes
var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Order, OrderDto>();
});

// Validate the configuration. Members present in the Destination must exist in the Source, 
// or be handled specifically; otherwise, an AutoMapperConfigurationException will be thrown.
config.AssertConfigurationIsValid();

// Create the Mapper
var mapper = config.CreateMapper();
// Create a Destination dest and map the source values to dest
Destination dest = mapper.Map<Destination>(source);

// Map the source values to an existing dest
mapper.Map(source, dest);

Configuration

Normally, the mapping relationships between classes do not change dynamically, so you only need to create one instance of MapperConfiguration. This is typically written in Startup.cs. In .NET 6, due to new C# syntax, Startup.cs is not provided by default, so it is written in Program.cs.

Common Mapping Configuration Between Classes

csharp
var config = new MapperConfiguration(cfg => {
    // Single type conversion setting, configuring that Source can be converted to Destination
    cfg.CreateMap<Source, Destination>();

    // Use ConvertUsing to define the conversion relationship between two types directly using Expression<Func<Source, Destination>>
    // Here it is used to trim whitespace from string values
    cfg.CreateMap<string?, string?>()
        .ConvertUsing(x => x == null ? x : x.Trim());

    // Referencing settings written in a Profile
    cfg.AddProfile(OtherProfile);
});

//...

// Write settings in a separate class for others to reference
public class OtherProfile : Profile {
 public OtherProfile() {
  cfg.CreateMap<Source1, Destination1>();
 }
}

Common Mapping Configuration Between Properties

Prefixes/Postfixes

csharp
var config = new MapperConfiguration(cfg => {
    // Single class setting, configuring that Source can be converted to Destination
    cfg.CreateMap<Source, Destination>();
    // Set source prefix
    // For example: Both Source.Name -> Destination.Name and Source.PrefixName -> Destination.Name are supported
    // If Source has both Name and PrefixName, the member defined first in Source takes precedence
    cfg.RecognizePrefixes("Prefix");

    // Set source postfix
    cfg.RecognizePostfixes("Postfix");

    // Set destination prefix
    // For example: Both Source.Name -> Destination.Name and Source.Name -> Destination.PrefixName are supported
    // If Destination has both Name and PrefixName, both members will receive the value of Source.Name
    cfg.RecognizeDestinationPrefixes("Prefix");

    // Set destination postfix
    cfg.RecognizeDestinationPostfixes("Postfix");

    // "Get" is added as a prefix by default; call this API if you do not want this prefix
    cfg.ClearPrefixes();
});

//......

// RecognizePrefixes reference principle
class Destination {
    public string Name { get; set; }
}

// If Source.Name is defined first
class Source {
    public string Name { get; set; } // Destination.Name gets the value of Source.Name
    public string PrefixName { get; set; }
}

// If Source.PrefixName is defined first
class Source {
    public string PrefixName { get; set; } // Destination.Name gets the value of Source.PrefixName
    public string Name { get; set; }
}

Configuring for a Single Member

csharp
var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Source, Destination>()
        // Do not map values to Destination.Prop1
        // Usually used when Destination has this member but Source does not
        .ForMember(desc => desc.Prop1, opt => opt.Ignore())

        // Source and Destination names are different, set the mapping relationship
        .ForMember(dest => dest.DestProp2, opt => opt.MapFrom(src => src.SourceProp2))

        // When mapping objects, do not take the value from Source, but set it explicitly
        .ForMember(desc => desc.DateProp3, opt => opt.MapFrom(src => DateTime.Now))

        // Only map to Destination.IntProp4 if Source.IntProp4 is greater than or equal to 0
        .ForMember(dest => dest.IntProp4, opt => opt.Condition(src => (src.IntProp4 >= 0)))

        // If Source.Prop5 is Null, set Destination.Prop5 to "Other Value"; otherwise, use the value from Source.Prop5
        .ForMember(dest => dest.Prop5, opt => opt.NullSubstitute("Other Value")))

        // If desc.CreatedTime is not default, set desc.ModifiedTime to the current time
        // If desc.CreatedTime is default, set desc.CreatedTime to the current time
        // Mainly used when writing to different fields for creation and modification; 
        // the CreatedTime used for checking existence must be mapped last
        .ForMember(desc => desc.ModifiedTime, opt => {
            opt.PreCondition((src, desc, context) => desc.CreatedTime != default);
            opt.MapFrom(src => DateTime.Now);
        })
        .ForMember(desc => desc.CreatedTime, opt => {
            opt.PreCondition((src, desc, context) => desc.CreatedTime == default);
            opt.MapFrom(src => DateTime.Now);
        });
});

class Source {
    public string? SourceProp2 { get; set; }
    public int IntProp4 { get; set; }
    public string? Prop5 { get; set; }
}

public class Destination {
    public string? Prop1 { get; set; }
    public string? Prop2 { get; set; }
    public DateTime DateProp3 { get; set; }
    public int IntProp4 { get; set; }
    public string? Prop5 { get; set; }
    public DateTime CreatedTime { get; set; }
    public DateTime? ModifiedTime { get; set; }
}

Reverse Mapping

If you want Source and Destination types to be mutually convertible, you can use the following two methods:

csharp
// Define the conversion relationship between Source and Destination separately
var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Source, Destination>();
    cfg.CreateMap<Destination, Source>();
});

//...
// Use reverse mapping
var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Source, Destination>()
        .ReverseMap();
});

There are two things to note when using reverse mapping; evaluate them before use:

  1. For complex conversions, you need to set ForPath to define the reverse conversion relationship.
  2. AssertConfigurationIsValid() does not work for reverse mapping.
csharp
var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Source, Destination>()
        .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.Prop1))
        .ForMember(dest => dest.Prop5, opt => opt.MapFrom(src => src.Prop3 + "," + src.Prop4))
        .ReverseMap()
        .ForPath(s => s.Prop3, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[0]))
        .ForPath(s => s.Prop4, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[1]));
});

var source = new Destination {
    Prop2 = "123",
    Prop5 = "111,222"
};

Source dest = mapper.Map<Source>(source);

// dest.Prop1 = "123" This mapping relationship is simple, so it can be reverse-converted without ForPath
// dest.Prop3 = "111" If ForPath is not set, it will be null
// dest.Prop4 = "222" If ForPath is not set, it will be null

public class Source {
    public string? Prop1 { get; set; }

    public string? Prop3 { get; set; }

    public string? Prop4 { get; set; }
}

public class Destination {
    public string? Prop2 { get; set; }

    public string? Prop5 { get; set; }
}

Dependency Injection

You need to install the NuGet package AutoMapper.Extensions.Microsoft.DependencyInjection.

.NET Core 3.x Startup.cs

csharp
public void ConfigureServices(IServiceCollection services) {
    // Register using Assembly
    services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/);

    // Register using the type belonging to the Assembly
    services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/);
}

.NET 6 Program.cs (Writing style when not using Startup by default)

csharp
// Register using Assembly
builder.Services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/);

// Register using the type belonging to the Assembly
builder.Services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/);

Injecting AutoMapper into a Service or Controller

csharp
public class EmployeesController {
 private readonly IMapper mapper;

 public EmployeesController(IMapper mapper) => this.mapper = mapper;
}

Changelog

  • 2022-10-24 Initial version of the document created.